import {Control} from "./Control";
import type {IControlParams} from "./Control";
import {Key} from "../Lock";
import {LonLat} from "../LonLat";
import {math} from "../index";
import {Quat} from "../math/Quat";
import {Ray} from "../math/Ray";
import {Sphere} from "../bv/Sphere";
import {Vec2} from "../math/Vec2";
import {Vec3} from "../math/Vec3";
import type {ITouchState} from "../renderer/RendererEvents";
import {Plane} from "../math/Plane";
import { createEvents, EventsHandler } from "../Events";

interface ITouchNavigationParams extends IControlParams {
    /**
     * Inertia factor.
     * Default: 0.007
     */
    inertia?: number;
    /**
     * Limit for tilt angle.
     * Default: 0.1
     */
    minSlope?: number;
    /**
     * Limit for touch jerk speed to prevent unexpected camera zooming.
     * Default: 0.3
     */
    jerkLimit?: number;
}

export type TouchNavigationEventsList = [
    "inertiamove",
    "drag",
    "doubletapzoom",
];

const TOUCH_NAVIGATION_EVENTS: TouchNavigationEventsList = [
    /**
     * Triggered on inertia rotate.
     * @event og.TouchNavigation#rotate
     */
    "inertiamove",

    /**
     * Triggered on touch drag/rotate/tilt/zoom.
     * @event og.TouchNavigation#drag
     */
    "drag",

    /**
     * Triggered on double tap zoom.
     * @event og.TouchNavigation#doubletapzoom
     */
    "doubletapzoom",
];
const DEFAULT_INERTIA = 0.007;
const DEFAULT_MIN_SLOPE = 0.1;
const DEFAULT_JERK_LIMIT = 0.3;

class TouchExt {
    public x: number;
    public y: number;
    public prev_x: number;
    public prev_y: number;
    public grabbedPoint: Vec3 | null;
    public grabbedSpheroid: Sphere;

    protected _vec: Vec2;
    protected _vecPrev: Vec2;


    constructor() {
        this.x = 0;
        this.y = 0;
        this.prev_x = 0;
        this.prev_y = 0;
        this.grabbedPoint = null;
        this.grabbedSpheroid = new Sphere();
        this._vec = new Vec2();
        this._vecPrev = new Vec2();
    }

    public get dY(): number {
        return this.y - this.prev_y;
    }

    public get dX(): number {
        return this.x - this.prev_x;
    }

    public get vec(): Vec2 {
        return this._vec.set(this.x, this.y);
    }

    public get vecPrev(): Vec2 {
        return this._vecPrev.set(this.prev_x, this.prev_y);
    }
}

/**
 * Touch pad planet camera dragging control.
 * @class
 * @extends {Control}
 * @param {ITouchNavigationParams} [options] - Touch navigation options:
 * @param {number} [options.inertia] - inertia factor. Default is 0.007
 * @param {number} [options.minSlope] - minimal slope for vertical camera movement. Default is 0.1
 * @param {number} [options.jerkLimit] - limit for touch jerk speed to prevent unexpected camera movement. Default is 0.08
 * @fires og.TouchNavigation#inertiamove
 * @fires og.TouchNavigation#drag
 * @fires og.TouchNavigation#doubletapzoom
 */
export class TouchNavigation extends Control {

    public grabbedPoint: Vec3;
    public inertia: number;
    public minSlope: number;
    public jerkLimit: number;
    public events: EventsHandler<TouchNavigationEventsList>;

    protected grabbedSpheroid: Sphere;
    protected qRot: Quat;
    protected scaleRot: number;
    protected rot: number;
    protected _eye0: Vec3;
    protected pointOnEarth: Vec3 | null;
    protected earthUp: Vec3 | null;
    protected touches: TouchExt[];
    protected _keyLock: Key;
    protected _touching: boolean;


    constructor(options: ITouchNavigationParams = {}) {
        super(options);

        this._name = "touchNavigation";

        this.events = createEvents<TouchNavigationEventsList>(TOUCH_NAVIGATION_EVENTS, this);
        this.grabbedPoint = new Vec3();
        this.inertia = options.inertia != undefined ? options.inertia : DEFAULT_INERTIA;
        this.minSlope = options.minSlope != undefined ? options.minSlope : DEFAULT_MIN_SLOPE;
        this.jerkLimit = options.jerkLimit != undefined ? options.jerkLimit : DEFAULT_JERK_LIMIT;
        this.grabbedSpheroid = new Sphere();
        this.planet = null;
        this.qRot = new Quat();
        this.scaleRot = 0;
        this.rot = 1;
        this._eye0 = new Vec3();

        this.pointOnEarth = null;
        this.earthUp = null;

        this.touches = [new TouchExt(), new TouchExt()];

        this._keyLock = new Key();

        this._touching = false;
    }

    override oninit() {
        if (this.renderer) {
            this.renderer.events.on("touchstart", this.onTouchStart, this);
            this.renderer.events.on("touchend", this.onTouchEnd, this);
            this.renderer.events.on("doubletouch", this.onDoubleTouch, this);
            this.renderer.events.on("touchcancel", this.onTouchCancel, this);
            this.renderer.events.on("touchmove", this.onTouchMove, this);
            this.renderer.events.on("draw", this.onDraw, this);
        }
    }

    override onadd(): void {
        if (this.planet?.camera) {
            this.planet.camera.events.on("flystart", this._onCameraFly);
        }
    }

    override onremove(): void {
        if (this.planet?.camera) {
            this.planet.camera.events.off("flystart", this._onCameraFly);
        }
    }

    private _onCameraFly = () => {
        this.stopRotation();
    };

    protected onTouchStart(e: ITouchState) {
        const handler = this.renderer!.handler;
        this._touching = true;
        if (e.sys!.touches.length === 2) {
            const t0 = this.touches[0];
            const t1 = this.touches[1];

            t0.x = (e.sys!.touches.item(0)!.clientX - e.sys!.offsetLeft) * handler.pixelRatio;
            t0.y = (e.sys!.touches.item(0)!.clientY - e.sys!.offsetTop) * handler.pixelRatio;
            t0.prev_x = t0.x;
            t0.prev_y = t0.y;

            t0.grabbedPoint = this.planet!.getCartesianFromPixelTerrain(new Vec2(t0.x, t0.y)) || null;

            t1.x = (e.sys!.touches.item(1)!.clientX - e.sys!.offsetLeft) * handler.pixelRatio;
            t1.y = (e.sys!.touches.item(1)!.clientY - e.sys!.offsetTop) * handler.pixelRatio;
            t1.prev_x = t1.x;
            t1.prev_y = t1.y;

            t1.grabbedPoint = this.planet!.getCartesianFromPixelTerrain(new Vec2(t1.x, t1.y)) || null;

            this.pointOnEarth = this.planet!.getCartesianFromPixelTerrain(
                this.renderer!.handler.getCenter()
            ) || null;

            if (this.pointOnEarth) {
                this.earthUp = this.pointOnEarth.normal();
            }

            if (t0.grabbedPoint && t1.grabbedPoint) {
                t0.grabbedSpheroid.radius = t0.grabbedPoint.length();
                t1.grabbedSpheroid.radius = t1.grabbedPoint.length();
                this.stopRotation();
            }
        } else if (e.sys!.touches.length === 1) {
            this._startTouchOne(e);
        }
    }

    protected _startTouchOne(e: ITouchState) {
        const t = this.touches[0];
        const handler = this.renderer!.handler;

        t.x = (e.sys!.touches.item(0)!.clientX - e.sys!.offsetLeft) * handler.pixelRatio;
        t.y = (e.sys!.touches.item(0)!.clientY - e.sys!.offsetTop) * handler.pixelRatio;
        t.prev_x = t.x;
        t.prev_y = t.y;

        // t.grabbedPoint = this.planet!.getCartesianFromPixelTerrain(e, true);
        t.grabbedPoint = this.planet!.getCartesianFromPixelTerrain(e) || null;
        this._eye0.copy(this.planet!.camera.eye);

        if (t.grabbedPoint) {
            t.grabbedSpheroid.radius = t.grabbedPoint.length();
            this.stopRotation();
        }
    }

    public stopRotation() {
        this.qRot.clear();
        this.planet!.layerLock.free(this._keyLock);
        this.planet!.terrainLock.free(this._keyLock);
        this.planet!._normalMapCreator.free(this._keyLock);
    }

    protected onDoubleTouch(e: ITouchState) {

        this.planet!.stopFlying();
        this.stopRotation();

        const p = this.planet!.getCartesianFromPixelTerrain(e);
        if (p) {
            const g = this.planet!.ellipsoid.cartesianToLonLat(p);
            this.planet!.flyLonLat(
                new LonLat(g.lon, g.lat, this.planet!.camera.eye.distance(p) * 0.57)
            );
            this.events.dispatch(this.events.doubletapzoom, this);
        }
    }

    protected onTouchEnd(e: ITouchState) {
        if (e.sys!.touches.length === 0) {
            this._touching = false;
        }

        if (e.sys!.touches.length === 1) {
            this._startTouchOne(e);
        }

        if (
            Math.abs(this.touches[0].x - this.touches[0].prev_x) < 3 &&
            Math.abs(this.touches[0].y - this.touches[0].prev_y) < 3
        ) {
            this.scaleRot = 0;
        }
    }

    protected onTouchCancel(e: ITouchState) {
    }

    protected onTouchMove(e: ITouchState) {
        let cam = this.planet!.camera;
        const handler = this.renderer!.handler;
        if (e.sys!.touches.length === 2) {
            this.renderer!.controlsBag.scaleRot = 1;

            let t0 = this.touches[0],
                t1 = this.touches[1];

            if (!t0.grabbedPoint || !t1.grabbedPoint) {
                return;
            }

            this.planet!.stopFlying();

            t0.prev_x = t0.x;
            t0.prev_y = t0.y;
            t0.x = (e.sys!.touches.item(0)!.clientX - e.sys!.offsetLeft) * handler.pixelRatio;
            t0.y = (e.sys!.touches.item(0)!.clientY - e.sys!.offsetTop) * handler.pixelRatio;

            t1.prev_x = t1.x;
            t1.prev_y = t1.y;
            t1.x = (e.sys!.touches.item(1)!.clientX - e.sys!.offsetLeft) * handler.pixelRatio;
            t1.y = (e.sys!.touches.item(1)!.clientY - e.sys!.offsetTop) * handler.pixelRatio;

            const middle = t0.vec.add(t1.vec).scale(0.5);
            const earthMiddlePoint = this.planet!.getCartesianFromPixelEllipsoid(
                middle
            );
            if (earthMiddlePoint) {
                this.pointOnEarth = earthMiddlePoint;

                const prevAngle = Math.atan2(t0.prev_y - t1.prev_y, t0.prev_x - t1.prev_x);
                const curAngle = Math.atan2(t0.y - t1.y, t0.x - t1.x);

                const deltaAngle = curAngle - prevAngle;
                const distanceToPointOnEarth = cam.eye.distance(this.pointOnEarth);

                const zoomCur = t0.vec.sub(t1.vec);
                const zoomPrev = t0.vecPrev.sub(t1.vecPrev);
                let scale = zoomCur.length() / zoomPrev.length();
                const jerkMax = 1 + this.jerkLimit;
                const jerkMin = 1 - this.jerkLimit;
                scale = scale > jerkMax ? jerkMax : scale < jerkMin ? jerkMin : scale;
                let d = distanceToPointOnEarth * -(1 - scale);
                cam.eye.addA(cam.getForward().scale(d));
                cam.rotateAround(-deltaAngle, false, this.pointOnEarth, this.earthUp!);

                const panCur = t0.vec.add(t1.vec).scale(0.5);
                const panPrev = t0.vecPrev.add(t1.vecPrev).scale(0.5);
                const panOffset = panCur.sub(panPrev).scale(-1);
                var l = 0.5 / distanceToPointOnEarth * cam._lonLat.height * math.RADIANS;
                if (l > 0.003) l = 0.003;
                cam.rotateHorizontal(l * -panOffset.x, false, this.pointOnEarth, this.earthUp!);
                cam.rotateVertical(l * -panOffset.y, this.pointOnEarth, this.minSlope);

                cam.checkTerrainCollision();
                cam.update();
                this.events.dispatch(this.events.drag, this);
            }
            this.scaleRot = 0;
        } else if (e.sys!.touches.length === 1) {
            let t = this.touches[0];

            t.prev_x = t.x;
            t.prev_y = t.y;
            t.x = (e.sys!.touches.item(0)!.clientX - e.sys!.offsetLeft) * handler.pixelRatio;
            t.y = (e.sys!.touches.item(0)!.clientY - e.sys!.offsetTop) * handler.pixelRatio;

            if (!t.grabbedPoint) {
                return;
            }

            this.planet!.stopFlying();

            let direction = e.direction
            let targetPoint = new Ray(cam.eye, direction).hitSphere(t.grabbedSpheroid);

            if (targetPoint) {
                if (cam.slope > 0.2) {
                    this.qRot = Quat.getRotationBetweenVectors(
                        targetPoint.normal(),
                        t.grabbedPoint.normal()
                    );
                    let rot = this.qRot;
                    cam.eye = rot.mulVec3(cam.eye);
                    cam.rotate(rot);
                    // cam._r = rot.mulVec3(cam._r);
                    // cam._u = rot.mulVec3(cam._u);
                    // cam._b = rot.mulVec3(cam._b);
                    cam.checkTerrainCollision();
                    cam.update();
                    this.events.dispatch(this.events.drag, this);
                    this.scaleRot = 1;
                } else {
                    let p0 = t.grabbedPoint,
                        p1 = Vec3.add(p0, cam.getUp()),
                        p2 = Vec3.add(p0, p0.getNormal());
                    let dir = cam.unproject(t.x, t.y);
                    let px = new Vec3();
                    if (new Ray(cam.eye, dir).hitPlaneRes(Plane.fromPoints(p0, p1, p2), px) === Ray.INSIDE) {
                        cam.eye = this._eye0.addA(px.subA(p0).negate());
                        cam.checkTerrainCollision();
                        cam.update();
                        this.events.dispatch(this.events.drag, this);
                        this.scaleRot = 0;
                    }
                }
            }
        }
    }

    protected onDraw() {

        const r = this.renderer!;

        r.controlsBag.scaleRot = this.scaleRot;

        if (this._touching) {
            return;
        }

        let cam = this.planet!.camera;
        let prevEye = cam.eye.clone();

        if (r.events.mouseState.leftButtonDown || !this.scaleRot) {
            return;
        }

        this.scaleRot -= this.inertia;
        if (this.scaleRot <= 0) {
            this.scaleRot = 0;
        } else {
            r.controlsBag.scaleRot = this.scaleRot;
            let rot = this.qRot
                .slerp(Quat.IDENTITY, 1 - this.scaleRot * this.scaleRot * this.scaleRot)
                .normalize();
            if (!(rot.x || rot.y || rot.z)) {
                this.scaleRot = 0;
            }
            cam.eye = rot.mulVec3(cam.eye);
            cam.rotate(rot);
            // cam._r = rot.mulVec3(cam._r);
            // cam._u = rot.mulVec3(cam._u);
            // cam._b = rot.mulVec3(cam._b);
            cam.checkTerrainCollision();
            cam.update();
            this.events.dispatch(this.events.inertiamove, this);
        }
        this.setLock(cam.eye.distance(prevEye) / cam.getAltitude() > 0.01);
    }

    private setLock(lock: boolean): void {
        if (lock) {
            this.planet!.layerLock.lock(this._keyLock);
            this.planet!.terrainLock.lock(this._keyLock);
            this.planet!._normalMapCreator.lock(this._keyLock);
        } else {
            this.planet!.layerLock.free(this._keyLock);
            this.planet!.terrainLock.free(this._keyLock);
            this.planet!._normalMapCreator.free(this._keyLock);
        }
    }
}
